鐵人賽終於來到最後三天。
到目前為止,
還有幾個想要談的東西還沒談到,
比如說,
昨天所介紹的行動翻譯做法,
主要是靠 Kiwi 瀏覽器達成。
如果是比較正規的做法,
應該是直接製作 App 才對。
如果要製作 App,就會用到 webview 的架構。
因此,
Webview 應該可做為我們其中一天的主題,
簡單談一下如何用它來實現行動翻譯 App 才對。
另外,目前我們的翻譯平台,
主要是利用 Javascript 做為前端界面,
後端則是採用 Python 的 flask 來提供服務。
雖然一般電腦就可以執行 flask 服務器,
但由於一般網路架構的限制,
目前我們的服務只能在 LAN 區網內使用。
換句話說,
如果你只是在家上個廁所,
想改用手機完成翻譯工作,應該沒什麼問題,
但如果離開家裡,到了 WAN 外網的環境,
想要連到家裡的伺服器就沒那麼簡單了。
為了讓我們的足跡,
可以從家裡的廁所擴展到全國各地的咖啡店(^_^)
我們勢必需要提供一些解決方案。
最簡單的做法,就是依然把伺服器放在家裡,
然後對家裡的網路進行一些設定。
另一種做法,則是運用雲端主機服務。
這裡其實也可以順便介紹一下 docker 的用法。
比如像這些相關的主題,
都很適合做為其中一天的主題,
各位只要跟著我們做一遍,
就會建立起相當不錯的實務經驗了。
而一旦翻譯服務能在網路提供服務,
讓許多人共同使用、交流,
想像空間就會瞬間爆炸,很多可能性就會蜂擁而出。
另一方面,
我們目前的資料結構仍舊非常原始。
因應一些靜態頁面時,問題還不大,
但如今許多網頁都有動態內容,
如果 HTML 結構稍有變化,
我們的資料結構處理起來就會捉襟見肘,問題叢生。
因此,重新整理資料的結構與保存的方式,
也是對這個專案很有幫助的發展方向。
還有,目前我們所建立的程式架構,
提供了許多好用的資源,
有待我們進一步善加利用。
另外還有一些更好的構想與資源,也有待引入。
身為譯者,幾乎隨時都有一些更好的想法與創意,
希望可以為這個專案添加更多好用的功能。
不止如此,
經過長期累積翻譯的經驗之後,
我也逐漸對翻譯這個領域,
有了一些希望可以振衰起敝的想法,
甚至覺得翻譯工作,
很可能是文化發展其中一個非常重要、
卻長期被忽略的關鍵。
加上這些年機器翻譯、人工智慧的發展,
更讓許多想法有了大幅的推展,
究竟哪些事可以利用科技發展做得更好,
哪些事是科技發展難以企及之所在,
譯者與機器學習、人工智慧,
如何從競爭轉為合作,
這些種種我也有許多想法想要分享。
鐵人賽剛開始時,
還有點擔心想談的內容不知道能不能撐滿三十天,
但如今再看卻發現,
想談的東西太多,時間卻太少。
而且這段期間,實在花了不少力氣編寫程式碼,
寫程式就是這樣,順的時候簡直快要飛起來,
遇到問題時卻只能一個頭兩個大,
有時甚至無法估計何時才能解決問題。
所幸這段期間所遇到的問題,
雖然讓進度有所延遲,但多半都能後補解決。
我目前雖然還在後補之前的一些坑洞,
但補著補著也快要追趕上來了。
今天的話,先談到這裡,
請容我再想想今天的內容,
可以用前面所說的什麼主題來補上,
不管怎麼說,
之後這裡一定要再給出
至少我自己都能覺得滿意的內容才行。
^_^
X X X
好。現在就來追補一下後續的內容吧。
原本我在想,用 webview 寫 App,就好像在寫自定義的瀏覽器。
雖然這個瀏覽器可以透過 Javascript(或 Java)程式碼,
放進一些我們想要的功能,
但這個瀏覽器並不會有 Chrome 或 Kiwi 瀏覽器那樣的內建翻譯功能。
或許這個用 webview 實現的瀏覽器可以內建翻譯的功能也說不定,
但我真的還不知道該怎麼做。
總之,如果要用 webview 實現我們的外掛功能,
原本可直接擷取翻譯的功能就形同虛設了。
基於這個理由,
究竟還要不要談 webview 的做法,一直讓我很猶豫。
畢竟 Kiwi 瀏覽器也已經滿足行動方面的需求了。
猶豫之下,我甚至還在想,
也許談談 Merkle Tree 這樣的其他話題,
看看網站內的重複內容如何援用之前的翻譯,
這也許還更加有意義呢!
後來我再一想,這話題一展開的話,
能談的東西也太多了。
而我們用 Alt+123 擷取譯文的做法,
在 webview App 裡雖然沒什麼用處了,
但我們還有 Alt+Shift+$ 一鍵取得翻譯的功能、
還有其他翻譯編輯查詢比對替換等主要功能,
這些全都還是可以正常運作呀!!
所以,把外掛化為 webview App,
應該還是有一定的用處才對。
況且這畢竟是比較正規的做法,
若講到行動應用,不談這個好像說不過去的感覺。
那就好吧,我們還是簡單介紹一下,
如何把我們的外掛功能,化為行動 App 的做法吧。
首先,當然要把 Android Studio 下載安裝起來 ^_^
然後建立一個新專案,姑且取名為 bt_app,
過程中選擇 Empty Activity 即可,
程式語言則選擇 Java(其實 kolin 也行)。
不過因為我們要使用到 webview,
為了免除安全性疑慮,請選擇採用 4.4 以上的 API。
雖然是空的專案,我們可以看到畫面的左方,
還是自動幫我們建立了一整組的專案目錄與檔案結構。
我們打算動到的地方共有四處:
首先在 AndroidManifest.xml 裡,加入網路權限:
<manifest ...>
...
<uses-permission android:name="android.permission.INTERNET" />
<application
...
</applicaion>
</manifest>
接著到 activity_main.xml 修改 layout 畫面的佈局方式。
請先切換到程式碼的畫面(點擊右上方的 Code),
清除掉原本所有的內容,再放入下面的 Webview:
<WebView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
這麼一來,這個 App 一啟動就只能瀏覽網頁了。
然後修改一下 themes.xml 裡的設定值,
把 style 標籤最後面原本的 DarkActionBar 改成 NoActionBar,
就可以去掉礙眼的標題列了:
<style name="Theme.Bt_app" parent="Theme.MaterialComponents.DayNight.NoActionBar">
這裡要特別提醒一下的是,
所採用的樣板會區分黑白模式,
所以同時存在兩個 themes.xml,
記得兩個 themes.xml 都要做修改喲!!
最後就是要建立 MainActivity.java 的程式碼了。
在 MainActibity 這個物件類別裡,
我們要覆寫(override)onCreate 的程式碼。
在 onCreate 函式內原本的程式碼下方,
我們加入下面的程式碼:
WebView webview = findViewById(R.id.webview);
WebSettings webSettings = webview.getSettings();
webSettings.setJavaScriptEnabled(true);
webSettings.setDomStorageEnabled(true);
setContentView(webview);
webview.setWebViewClient(new WebViewClient());
webview.loadUrl("https://www.google.com.tw/");
簡單說,這裡就是把 webview 設定為 App 的一個 View,
並啟用 Javascript 和 localStorage,
然後自動開啟 Google 的首頁。
程式碼其中有很多地方,會以紅色字體顯示。
如果把鍵盤或滑鼠游標移動到紅色字體處,畫面就會顯示相應的說明,
大體上就是說「需要匯入相應的物件類別」之類的訊息。
不只如此,訊息還很貼心的提醒你,只要按下【Alt+Enter】快速鍵,
就會在程式碼相應的位置,自動匯入所需的東西。
這部分請自行處理,這裡就不再多做說明了。
(共需匯入 WebView、WebSettings、WebViewClient 三個物件類別)
到此為止,我們就可以開始嘗試執行這個 App 了。
只要按下【 Shift+F10】快速鍵,或點擊畫面上方綠色的三角形執行鍵,
系統就會啟動一個 Android 模擬器,並執行你剛才完成的 App。
你也可以在手機或平板的開發人員選項中,
開啟 USB 偵錯模式,然後用 USB 接上電腦,
就可以直接在手機或平板上直接測試 App 的執行狀況了。
這就是一個最簡單的 Webview App。
它現在一執行就會進入 Google 首頁,
我們可以透過 Google 搜索的功能,
進入想前往的網頁。
但是,只要按下【退回鍵】,
就會退出這個 App。
這並不是我們想要的行為,
所以我們再修改一下程式碼,
在 MainActivity.java 裡覆寫一下 onKeyDown() 函式:
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
WebView webview = findViewById(R.id.webview);
if (keyCode == KeyEvent.KEYCODE_BACK && webview.canGoBack()) {
webview.goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
程式碼裡的 KeyEvent 出現紅字。。。你還記得怎麼處理吧!
修改過程式碼之後,我們改用【Ctrl+F10】執行 App,
這樣才會套入剛才的變動,執行新的程式碼。
結果可以看到,【退回鍵】總算正常運作了。
這個類似瀏覽器功能的 App,還有另一個問題。
當你在其中瀏覽了一些網頁之後,
如果想要換個姿勢,把手機從直屏換成橫屏,
結果手機一旋轉方向,就會立刻回到 App 首頁的畫面了!
這絕不是我們想要的行為,所以也要修改一下。
修改方式也很簡單,只要來到 AndroidManifest.xml,
在其中的 activity 標籤中,添加下面的設定即可:
<activity
...
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
...>
在這樣的設定下,虛擬鍵盤也不會造成
「回到首頁」的問題了(蛤?本來會這樣哦?)
另外,我們現在這樣的做法,
等於是把 Google 的首頁當成 App 的首頁。
雖然 Google 也許沒什麼意見,
但如果你想用自己的頁面當首頁,
只要在前面的 webview.loadUrl(...) 放入自己的頁面網址即可。
還有另一種做法,就是建立一個 assets 目錄,然後在其中建立一個 html 子目錄,
並在裡頭建立一個本地的 html 檔案。
接著只要透過 webview.loadUrl('file:///android_asset/html/xxx.html'),
就可以把本地目錄中的 xxx.html 當成 App 的首頁了。
到這裡為止,我們都還沒討論到如何載入外掛。
接著就來看看怎麼做吧。
webview 要載入外掛,主要就是要載入外掛的 content.js,
以及其他針對網頁【原始內容】所要執行的 js 檔案(以及 css 檔案)。
至於【彈出頁面】、【背景服務】所負責的工作,我們稍後再談。
目前與【原始內容】相關的 js 檔案總共有 7 個:
另外還有兩個 css 檔案:
要把這些檔案載入 App,必須先做些整理。
最重要的是,載入的 js 檔案必須遵守以下的規則:
其實這也正是壓縮 js 檔案時必須遵守的條件。
最主要是因為壓縮時,會把換行符號去除,
一旦去除了換行符號,
【// 行內註解】【行尾沒有 ; 分號】這兩種情況,
可能就會造成程式碼解析的問題。
倒過來說,我們只要把 js 程式碼壓縮起來,
就可以直接載入 App 了。
如果要壓縮 js/css 檔案,可參考 VScode 裡的 Minify 擴充功能
處理好 js 和 css 檔案之後,
我們就可以把這些檔案載入 App 了。
首先我們會先在 assets 目錄下,
分別建立 js 和 css 目錄,然後把檔案放進相應的目錄中。
接著在 onCreate() 的程式碼裡,
設定一個可協助處理 Javascript 的 WebChromeClient,
然後再設定另一個自定義的 btAppWebViewClient:
@Override
protected void onCreate(Bundle savedInstanceState) {
...
webview.setWebChromeClient(new WebChromeClient());
webview.setWebViewClient(new btAppWebViewClient());
...
緊接著在 onCreate() 函式的下方,
我們就可以建立這個只供內部使用的 btAppWebViewClient 子類別。
它是繼承自原始的 WebViewClient,
然後再覆寫其中的 onPageFinished(),
它會等到原始頁面載入完成之後,才載入外掛的 js 和 css。
private class btAppWebViewClient extends WebViewClient {
@Override
public void onPageFinished(WebView view, String url) {
...
view.loadUrl("javascript: "+assetfile2string("js/content.js"));
...
super.onPageFinished(view, url);
}
}
在上面的程式碼中可以看到,
我們同樣是用 mWebView.loadUrl(...) 這個函式,
來載入 js 程式碼。
其中的 assetfile2string 函式,會把檔案轉換成字串:
private String assetfile2string(String filename) {
StringBuilder sb = new StringBuilder();
InputStream inputStream;
BufferedReader br;
String str;
try {
inputStream = getAssets().open(filename);
br = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
while ((str = br.readLine()) != null) {
sb.append(str);
}
br.close();
} catch (IOException ignored) {
System.out.println(ignored.getMessage());
return "alert(\"載入 assets 檔案出錯了...\");";
}
return sb.toString();
}
若要載入 css 檔案,也可以利用類似 js 的做法。
我們特別針對 CSS 的載入,寫了一個專用的函式:
private void injectCSS(WebView webview, String filename) {
try {
InputStream inputStream = getAssets().open(filename);
byte[] buffer = new byte[inputStream.available()];
inputStream.read(buffer);
inputStream.close();
String encoded = Base64.encodeToString(buffer, Base64.NO_WRAP);
webview.loadUrl("javascript:(function() {" +
"var parent = document.getElementsByTagName('head').item(0);" +
"var style = document.createElement('style');" +
"style.type = 'text/css';" +
"style.innerHTML = window.atob('" + encoded + "');" +
"parent.appendChild(style)" +
"})()");
} catch (Exception e) {
e.printStackTrace();
}
}
這樣一來,就可以把 js 和 css 載入頁面了。
至於【彈出頁面】所負責的工作,
除了顯示一些資訊之外,就是接收 API_KEY 的輸入。
而【背景服務】所負責的工作,則主要是與外部聯繫。
這些工作全都可由 Android 的 JAVA 程式碼完成。
但 JAVA 程式碼的實現方式,
這裡若要再進一步展開,一時之間恐怕也談不完,
所以今天就先談到這裡,
他日若還有機會,我們再進一步說明囉。。。
綜合以上所述,
用 Android 的 webview 來實現外掛的功能,
實在是非常花功夫的一件事。
這也就是我發現 Kiwi 瀏覽器可以直接載入外掛使用時,
感動到快要哭出來的主要原因之一呀。
不過,對於 Web App 來說,webview 確實相當好用。
有興趣的同學,當然可以再多花點時間,
繼續研究這個主題囉。